package storage

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"path/filepath"
	"strings"

	"gitee.com/thubcc/p2pgit/types"
	"gopkg.in/src-d/go-git.v4/plumbing"
	"gopkg.in/src-d/go-git.v4/plumbing/filemode"
	"gopkg.in/src-d/go-git.v4/plumbing/object"
)

const (
	maxTreeDepth      = 1024
	startingStackSize = 8
)

// New errors defined by this package.
var (
	ErrMaxTreeDepth      = errors.New("maximum tree depth exceeded")
	ErrFileNotFound      = errors.New("file not found")
	ErrDirectoryNotFound = errors.New("directory not found")
	ErrEntryNotFound     = errors.New("entry not found")
)

// Tree is basically like a directory - it references a bunch of other trees
// and/or blobs (i.e. files and sub-directories)
type Tree struct {
	Entries []object.TreeEntry
	Hash    types.Hash

	s ObjectStorer
	m map[string]*object.TreeEntry
	t map[string]*Tree // tree path cache
}

// GetTree gets a tree from an object storer and decodes it.
func GetTree(s ObjectStorer, h types.Hash) (*Tree, error) {
	o, err := s.GetRawObject(h)
	if err != nil {
		return nil, err
	}

	return DecodeTree(s, o)
}

// DecodeTree decodes an encoded object into a *Tree and associates it to the
// given object storer.
func DecodeTree(s ObjectStorer, o *RawObject) (*Tree, error) {
	t := &Tree{s: s}
	if err := t.Decode(o); err != nil {
		return nil, err
	}

	return t, nil
}

// Decode transform an plumbing.EncodedObject into a Tree struct
func (t *Tree) Decode(o *RawObject) (err error) {
	if o.Type() != plumbing.TreeObject {
		return object.ErrUnsupportedObject
	}

	t.Hash = o.Hash()
	if o.Size() == 0 {
		return nil
	}

	t.Entries = nil
	t.m = nil

	reader := o.Reader()
	if err != nil {
		return err
	}

	r := bufPool.Get().(*bufio.Reader)
	defer bufPool.Put(r)
	r.Reset(reader)
	for {
		str, err := r.ReadString(' ')
		if err != nil {
			if err == io.EOF {
				break
			}

			return err
		}
		str = str[:len(str)-1] // strip last byte (' ')

		mode, err := filemode.New(str)
		if err != nil {
			return err
		}

		name, err := r.ReadString(0)
		if err != nil && err != io.EOF {
			return err
		}

		var hash types.Hash
		if _, err = io.ReadFull(r, hash[:]); err != nil {
			return err
		}

		baseName := name[:len(name)-1]
		t.Entries = append(t.Entries, object.TreeEntry{
			Hash: plumbing.Hash(hash),
			Mode: mode,
			Name: baseName,
		})
	}

	return nil
}
func (t *Tree) entry(baseName string) (*object.TreeEntry, error) {
	if t.m == nil {
		t.buildMap()
	}

	entry, ok := t.m[baseName]
	if !ok {
		return nil, ErrEntryNotFound
	}

	return entry, nil
}
func (t *Tree) buildMap() {
	t.m = make(map[string]*object.TreeEntry)
	for i := 0; i < len(t.Entries); i++ {
		t.m[t.Entries[i].Name] = &t.Entries[i]
	}
}

// FindEntry search a TreeEntry in this tree or any subtree.
func (t *Tree) FindEntry(path string) (*object.TreeEntry, error) {
	if t.t == nil {
		t.t = make(map[string]*Tree)
	}

	pathParts := strings.Split(path, "/")
	startingTree := t
	pathCurrent := ""

	// search for the longest path in the tree path cache
	for i := len(pathParts) - 1; i > 1; i-- {
		path := filepath.Join(pathParts[:i]...)

		tree, ok := t.t[path]
		if ok {
			startingTree = tree
			pathParts = pathParts[i:]
			pathCurrent = path

			break
		}
	}

	var tree *Tree
	var err error
	for tree = startingTree; len(pathParts) > 1; pathParts = pathParts[1:] {
		if tree, err = tree.dir(pathParts[0]); err != nil {
			return nil, err
		}

		pathCurrent = filepath.Join(pathCurrent, pathParts[0])
		t.t[pathCurrent] = tree
	}

	return tree.entry(pathParts[0])
}
func (t *Tree) dir(baseName string) (*Tree, error) {
	entry, err := t.entry(baseName)
	if err != nil {
		return nil, ErrDirectoryNotFound
	}

	obj, err := t.s.GetRawObject(types.Hash(entry.Hash))
	if err != nil {
		return nil, err
	}

	tree := &Tree{s: t.s}
	err = tree.Decode(obj)

	return tree, err
}

// Tree returns the tree identified by the `path` argument.
// The path is interpreted as relative to the tree receiver.
func (t *Tree) Tree(path string) (*Tree, error) {
	e, err := t.FindEntry(path)
	if err != nil {
		return nil, ErrDirectoryNotFound
	}

	tree, err := GetTree(t.s, types.Hash(e.Hash))
	if err == plumbing.ErrObjectNotFound {
		return nil, ErrDirectoryNotFound
	}

	return tree, err
}

func entry2String(e *object.TreeEntry) string {
	return fmt.Sprintf("%0x %s %s", e.Hash, e.Mode.String(), e.Name)
}

func (t *Tree) String() string {
	r := ""
	for _, e := range t.Entries {
		r += entry2String(&e) + "\n"
	}
	return r
}

// File represents git file objects.
type File struct {
	// Name is the path of the file. It might be relative to a tree,
	// depending of the function that generates it.
	Name string
	// Mode is the file mode.
	Mode filemode.FileMode
	// Blob hash.
	Hash types.Hash

	s ObjectStorer
	o *RawObject
}

// // NewFile returns a File based on the given blob object
// func (t *Tree) NewFile(name string, m filemode.FileMode, h types.Hash) *File {
// 	return &File{Name: name, Mode: m, Hash: h, s: t.s}
// }

// TreeEntryFile returns the *File for a given *TreeEntry.
func (t *Tree) TreeEntryFile(e *object.TreeEntry) *File {
	return &File{
		Name: e.Name,
		Mode: e.Mode,
		Hash: types.Hash(e.Hash),
		s:    t.s,
	}
}

// Blob file's blob
func (f *File) Blob() (o *RawObject, err error) {
	if f.o == nil {
		f.o, err = f.s.GetRawObject(f.Hash)
	}
	o = f.o
	return
}

// Size file's size
func (f *File) Size() int {
	if f.o == nil {
		f.o, _ = f.s.GetRawObject(f.Hash)
	}
	return f.o.Size()
}

// Bytes file's raw
func (f *File) Bytes() []byte {
	if f.o == nil {
		f.o, _ = f.s.GetRawObject(f.Hash)
	}
	return f.o.Bytes()
}
